/*
 * Copyright 2024 the KubeSphere Authors.
 * Please refer to the LICENSE file in the root directory of the project.
 * https://github.com/kubesphere/kubesphere/blob/master/LICENSE
 */

package token

import (
	"encoding/base64"
	"reflect"
	"testing"
	"time"

	"github.com/go-jose/go-jose/v4"
	"github.com/golang-jwt/jwt/v4"
	"github.com/stretchr/testify/assert"
	"k8s.io/apiserver/pkg/authentication/user"

	"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
)

const privateKeyData = `
-----BEGIN RSA PRIVATE KEY-----
MIIEoQIBAAKCAQEAnDK2bNmX+tBWY/JHll1T1LF3/6RTbJ2qUsvwZVuVP/XbmWeY
vDZyTR+YL6JaqRC/NibphgCV0p6cKZNuoGCEHpS0Ix9ZZwkA8BhwrFwAU0O1Qmrv
v7It3p0Lc9WKN7PBWDQnUIdeSWnAeSbmWETP8Y2e+vG/iusLojPJenEaiOiwzU8p
3CGSh7IPBXF3aUeB3dgJuaiumDuzlp0Oe/xKvWo0faB2hFXi36KaLMpugNcbejKl
R6w3jH5wJjto5XqTEpW4a77K4rt7CFXVGfcbLo+n/5j3oC0lw4KOy7OX0Qf1jY+x
sa1Q+3UoDC02sQRf77uj3eITol8Spoo7wfJqmwIDAQABAoIBAGEArYIT7+p3j+8p
+4NKGlGwlRFR/+0oTSp2NKj9o0bBbMtsJtJcDcgPoveSIDN2jwkWSVhK7MCMd/bp
9H3s8p/7QZO+WEtAsDBrPS4NRLZxChRhTNsD0LC7Xu1k5B2LqLsaSIAeUVPONRYI
Lm0K7wjYJq85iva+2c610p4Tt6LlxuOu41Zw7RAaW8nBoMdQzi19X+hUloogVo7S
hid8gm2KUPY6xF+RpHGQ5OUND0d+2wBkHxbYNRIfYrxCKt8+dLykLzAmm+ScCfyG
jKcNoRwW5s/3ttR7r7hn3whttydkper5YvxM3+EvL83H7JL11KHcHy/yPYv+2IxQ
psvEtIECgYEAykCm/w58pdifLuWG1mwHCkdQ6wCd9gAHIDfaqbFhTcdwGGYXb1xQ
3CHjkkX6rpB3rJReCxlGq01OemVNlpIGLhdnK87aX5pRVn2fHGaMoC2V5RWv3pyE
3gJ41h9FtPX2juKFG9PNiR7FrtKPzQczfh2L1OMpLOXfPgxvo/fXBQsCgYEAxbTz
mibb4F/TBVXMuSL7Pk9hBPlFgFIEUKbiqt+sKQCqSZPGjV5giDfQDGsJ5EqOkRg0
qlCrKk+DW+d+Pmc4yo58cd2xPnQETholV19+dM3AGiy4BMjeUnJD+Dme7M/fhrlW
IK/1ZErKSZ3nN20qeneIFltm6+4pgQ1HB9KwirECgYAy65wf0xHm32cUc41DJueO
2u2wfPNIIDGrFuTinFoXLwM14V49F0z0X0Pga+X1VUIMHT6gJLj6H/iGMEMciZ8s
s4+yI94u+7dGw1Hv4JG/Mjru9krVDSsWiiDKKA1wxgxRZQ6GNwkkYK78mN7Di/CW
6/Fso9SWDTnrcU4aRifIiQKBgQCQ+kJwVfKCtIIPtX0sfeRzKs5gUVKP6JTVd6tb
1i1u29gDoGPHIt/yw8rCcHOOfsXQzElCY2lA25HeAQFoTVUt5BKJhSIGRBksFKwx
SAt5J6+pAgXnLE0rdDM3gTlzOnQVXS81RRLTeqygEzSMRncR2zll+5ybgcfZpJzj
tbJT4QJ/Y02wfkm1dL/BFg520/otVeuC+Bt+YyWMVs867xLLzFci7tj6ZzlzMorQ
PsSsOHhPx0g+Wl8K2+Edg3FQRZ1m0rQFAZn66jd96u85aA9NH/bw3A3VYUdVJyHh
4ZgZLx9JMCkmRfa7Dp2mzoqGUC1cjNvm722baeMqXpHSXDP2Jg==
-----END RSA PRIVATE KEY-----
`

func TestNewIssuer(t *testing.T) {
	signKeyData := base64.StdEncoding.EncodeToString([]byte(privateKeyData))
	config := &oauth.IssuerOptions{
		URL:              "https://ks-console.kubesphere-system.svc",
		SignKeyData:      signKeyData,
		MaximumClockSkew: 10 * time.Second,
		JWTSecret:        "test-secret",
	}
	got, err := NewIssuer(config)
	if err != nil {
		t.Fatal(err)
	}

	signKey, keyID, err := loadSignKey(config)
	if err != nil {
		t.Fatal(err)
	}

	want := &issuer{
		url:              config.URL,
		secret:           []byte(config.JWTSecret),
		maximumClockSkew: config.MaximumClockSkew,
		signKey: &Keys{
			SigningKey: &jose.JSONWebKey{
				Key:       signKey,
				KeyID:     keyID,
				Algorithm: jwt.SigningMethodRS256.Alg(),
				Use:       "sig",
			},
			SigningKeyPub: &jose.JSONWebKey{
				Key:       signKey.Public(),
				KeyID:     keyID,
				Algorithm: jwt.SigningMethodRS256.Alg(),
				Use:       "sig",
			},
		},
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("NewIssuerOptions() got = %v, want %v", got, want)
		return
	}
}

func TestNewIssuerGenerateSignKey(t *testing.T) {
	config := &oauth.IssuerOptions{
		URL:              "https://ks-console.kubesphere-system.svc",
		MaximumClockSkew: 10 * time.Second,
		JWTSecret:        "test-secret",
	}

	got, err := NewIssuer(config)
	if err != nil {
		t.Fatal(err)
	}

	iss := got.(*issuer)
	assert.NotNil(t, iss.signKey)
	assert.NotNil(t, iss.signKey.SigningKey)
	assert.NotNil(t, iss.signKey.SigningKeyPub)
	assert.NotNil(t, iss.signKey.SigningKey.KeyID)
	assert.NotNil(t, iss.signKey.SigningKeyPub.KeyID)
}

func Test_issuer_IssueTo(t *testing.T) {
	type fields struct {
		url              string
		secret           []byte
		maximumClockSkew time.Duration
	}
	type args struct {
		request *IssueRequest
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    *VerifiedResponse
		wantErr bool
	}{
		{
			name: "token is successfully issued",
			fields: fields{
				url:              "kubesphere",
				secret:           []byte("kubesphere"),
				maximumClockSkew: 0,
			},
			args: args{request: &IssueRequest{
				User: &user.DefaultInfo{
					Name: "user1",
				},
				Claims: Claims{
					TokenType: AccessToken,
				},
				ExpiresIn: 2 * time.Hour},
			},
			want: &VerifiedResponse{
				User: &user.DefaultInfo{
					Name: "user1",
				},
				Claims: Claims{
					Username:  "user1",
					TokenType: AccessToken,
				},
			},
			wantErr: false,
		},
		{
			name: "token is successfully issued",
			fields: fields{
				url:              "kubesphere",
				secret:           []byte("kubesphere"),
				maximumClockSkew: 0,
			},
			args: args{request: &IssueRequest{
				User: &user.DefaultInfo{
					Name: "user2",
				},
				Claims: Claims{
					Username:  "user2",
					TokenType: RefreshToken,
				},
				ExpiresIn: 0},
			},
			want: &VerifiedResponse{
				User: &user.DefaultInfo{
					Name: "user2",
				},
				Claims: Claims{
					Username:  "user2",
					TokenType: RefreshToken,
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &issuer{
				url:              tt.fields.url,
				secret:           tt.fields.secret,
				maximumClockSkew: tt.fields.maximumClockSkew,
			}
			token, err := s.IssueTo(tt.args.request)
			if (err != nil) != tt.wantErr {
				t.Errorf("IssueTo() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			got, err := s.Verify(token)
			if (err != nil) != tt.wantErr {
				t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got == nil {
				return
			}
			assert.Equal(t, got.TokenType, tt.want.TokenType)
			assert.Equal(t, got.Issuer, tt.fields.url)
			assert.Equal(t, got.Username, tt.want.Username)
			assert.Equal(t, got.Subject, tt.want.User.GetName())
			assert.NotZero(t, got.IssuedAt)
		})
	}
}

func Test_issuer_Verify(t *testing.T) {
	type fields struct {
		url              string
		secret           []byte
		maximumClockSkew time.Duration
	}
	type args struct {
		token string
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    *VerifiedResponse
		wantErr bool
	}{
		{
			name: "token validation failed",
			fields: fields{
				url:              "kubesphere",
				secret:           []byte("kubesphere"),
				maximumClockSkew: 0,
			},
			args:    args{token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwidG9rZW5fdHlwZSI6ImFjY2Vzc190b2tlbiIsImV4cCI6MTYzMDY0MDMyMywiaWF0IjoxNjMwNjM2NzIzLCJpc3MiOiJrdWJlc3BoZXJlIiwibmJmIjoxNjMwNjM2NzIzfQ.4ENxyPTIe-BoQfuY5F4Mon5tB3KeV06B4i2JITRlPA8"},
			wantErr: true,
		},
		{
			name: "token is successfully verified",
			fields: fields{
				url:              "kubesphere",
				secret:           []byte("kubesphere"),
				maximumClockSkew: 0,
			},
			args: args{token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzA2MzczOTgsImlzcyI6Imt1YmVzcGhlcmUiLCJzdWIiOiJ1c2VyMiIsInRva2VuX3R5cGUiOiJyZWZyZXNoX3Rva2VuIiwidXNlcm5hbWUiOiJ1c2VyMiJ9.vqPczw4SyytVOQmgaK9ip2dvg2fSQStUUE_Y7Ts45WY"},
			want: &VerifiedResponse{
				User: &user.DefaultInfo{
					Name: "user2",
				},
				Claims: Claims{
					Username:  "user2",
					TokenType: RefreshToken,
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &issuer{
				url:              tt.fields.url,
				secret:           tt.fields.secret,
				maximumClockSkew: tt.fields.maximumClockSkew,
			}
			got, err := s.Verify(tt.args.token)
			if (err != nil) != tt.wantErr {
				t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got == nil {
				return
			}
			assert.Equal(t, got.TokenType, tt.want.TokenType)
			assert.Equal(t, got.Issuer, tt.fields.url)
			assert.Equal(t, got.Username, tt.want.Username)
			assert.Equal(t, got.Subject, tt.want.User.GetName())
			assert.NotZero(t, got.IssuedAt)
		})
	}
}

func Test_issuer_keyFunc(t *testing.T) {
	type fields struct {
		//nolint:unused
		name   string
		secret []byte
		//nolint:unused
		maximumClockSkew time.Duration
	}
	type args struct {
		token *jwt.Token
	}
	tests := []struct {
		name   string
		fields fields
		args   args
	}{
		{
			name: "sign key obtained successfully",
			fields: fields{
				secret: []byte("kubesphere"),
			},
			args: args{token: &jwt.Token{
				Method: jwt.SigningMethodHS256,
				Header: map[string]interface{}{"alg": "HS256"},
			}},
		},
		{
			name:   "sign key obtained successfully",
			fields: fields{},
			args: args{token: &jwt.Token{
				Method: jwt.SigningMethodRS256,
				Header: map[string]interface{}{"alg": "RS256"},
			}},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, err := NewIssuer(oauth.NewIssuerOptions())
			if err != nil {
				t.Error(err)
				return
			}
			iss := s.(*issuer)
			got, _ := iss.keyFunc(tt.args.token)
			assert.NotNil(t, got)
		})
	}
}
